package com.nageoffer.jobtrain.projects.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.nageoffer.jobtrain.common.web.exception.ClientException;
import com.nageoffer.jobtrain.projects.common.biz.user.UserContext;
import com.nageoffer.jobtrain.projects.common.constant.RedisCacheConstant;
import com.nageoffer.jobtrain.projects.common.enums.UserContestSubmitEnums;
import com.nageoffer.jobtrain.projects.dao.entity.ContestDO;
import com.nageoffer.jobtrain.projects.dao.entity.ContestQuestionDO;
import com.nageoffer.jobtrain.projects.dao.entity.ContestRemindDO;
import com.nageoffer.jobtrain.projects.dao.entity.UserContestDO;
import com.nageoffer.jobtrain.projects.dao.mapper.ContestMapper;
import com.nageoffer.jobtrain.projects.dao.mapper.ContestQuestionMapper;
import com.nageoffer.jobtrain.projects.dao.mapper.ContestRemindMapper;
import com.nageoffer.jobtrain.projects.dao.mapper.UserContestMapper;
import com.nageoffer.jobtrain.projects.dto.req.*;
import com.nageoffer.jobtrain.projects.dto.resp.*;
import com.nageoffer.jobtrain.projects.mq.event.ContestRemindDelayEvent;
import com.nageoffer.jobtrain.projects.mq.producer.ContestRemindDelayProducer;
import com.nageoffer.jobtrain.projects.service.ContestService;
import com.nageoffer.jobtrain.projects.service.UserContestService;
import com.nageoffer.jobtrain.projects.service.handler.remind.dto.ContestRemindDTO;
import com.nageoffer.jobtrain.projects.toolkit.ContestRemindUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 竞赛管理接口实现层
 * author：唐晨
 */
@Service
@RequiredArgsConstructor
public class ContestServiceImpl extends ServiceImpl<ContestMapper, ContestDO> implements ContestService {
    private final UserContestService userContestService;
    private final ContestMapper contestMapper;
    private final ContestRemindMapper contestRemindMapper;
    private final ContestRemindDelayProducer contestRemindDelayProducer;
    private final StringRedisTemplate stringRedisTemplate;
    private final ContestQuestionMapper contestQuestionMapper;
    private final UserContestMapper userContestMapper;

    @Override
    public IPage<ContestPageRespDTO> contestPageList(ContestPageReqDTO requestParam) {
        Page<ContestPageRespDTO> page = new Page<>(requestParam.getCurrent(), requestParam.getSize());
        List<ContestPageRespDTO> result = contestMapper.contestPageList(requestParam.getTitle(),
                requestParam.getBeginTime(), requestParam.getEndTime(), requestParam.getType(), page);
        // 判断该竞赛是否报名逻辑字段填充，并填充是否已经提交字段
        handleUserContestEnroll(result);
        page.setRecords(result);
        return page;
    }

    private void handleUserContestEnroll(List<ContestPageRespDTO> allContestList) {
        List<Long> userContestList = userContestService.listEnrollContest().stream()
                .map(UserContestRespDTO::getId)
                .collect(Collectors.toList());
        if (CollectionUtil.isEmpty(userContestList)) {
            return;
        }
        for (ContestPageRespDTO contest : allContestList) {
            if (userContestList.contains(contest.getId())) {
                contest.setIsEnroll(true);
                // 只有报名了的竞赛，才有可能完成竞赛提交功能
                LambdaQueryWrapper<UserContestDO> userContestQueryWrapper = Wrappers.lambdaQuery(UserContestDO.class)
                        .select(UserContestDO::getSubmitPapers)
                        .eq(UserContestDO::getContestId, contest.getId())
                        .eq(UserContestDO::getPhoneNumber, UserContext.getUsername())
                        .eq(UserContestDO::getDelFlag, 0);
                UserContestDO userContestDO = userContestMapper.selectOne(userContestQueryWrapper);
                if (ObjectUtil.equal(userContestDO.getSubmitPapers(), UserContestSubmitEnums.SUBMITTING.getType())) {
                    contest.setIsFinish(true);
                }
            }
        }
    }

    @Override
    public void createRemindContest(ContestCreateRemindReqDTO requestParam) {
        // 判断该竞赛是否存在或者是已发布
        LambdaQueryWrapper<ContestDO> ContestQueryWrapper = Wrappers.lambdaQuery(ContestDO.class)
                .eq(ContestDO::getId, requestParam.getContestId())
                .eq(ContestDO::getStatus, 1)
                .eq(ContestDO::getDelFlag, 0);
        ContestDO contestDO = contestMapper.selectOne(ContestQueryWrapper);
        if (contestDO == null) {
            throw new ClientException("该竞赛详情不存在/未发布");
        }
        Date now = new Date();
        // 计算预约时间（分钟）转换为毫秒
        long remindTimeInMillis = requestParam.getRemindTime() * 60 * 1000;

        // 判断该竞赛是否已经开赛
        if (contestDO.getBeginTime().before(now)) {
            throw new ClientException("该竞赛已经开赛了，抱歉无法预约！");
        }

        // 10分钟对应的毫秒数
        long tenMinutesInMillis = 10 * 60 * 1000;

        // 判断距离开赛是否小于10分钟
        if (contestDO.getBeginTime().getTime() - now.getTime() < tenMinutesInMillis) {
            throw new ClientException("距离竞赛开始不足10分钟，抱歉无法预约！");
        }

        // 判断开赛时间距离当前时间是否小于预约时间
        if (contestDO.getBeginTime().getTime() - now.getTime() < remindTimeInMillis) {
            throw new ClientException("开赛时间小于预约时间，请选择更短的提醒时间！");
        }

        // 判断该用户是否已经预约过该竞赛，如果已经预约过该竞赛，直接添加预约信息即可 -底层有唯一索引兜底
        LambdaQueryWrapper<ContestRemindDO> contestRemindQueryWrapper = Wrappers.lambdaQuery(ContestRemindDO.class)
                .eq(ContestRemindDO::getPhoneNumber, UserContext.getUsername())
                .eq(ContestRemindDO::getContestId, requestParam.getContestId());
        ContestRemindDO contestRemindDO = contestRemindMapper.selectOne(contestRemindQueryWrapper);

        // 此时用户对该竞赛的预约信息不存在，直接新建预约信息并添加
        if (contestRemindDO == null) {
            contestRemindDO = ContestRemindDO.builder()
                    .contestId(requestParam.getContestId())
                    // 设置竞赛开始时间
                    .startTime(contestDO.getBeginTime())
                    .phoneNumber(UserContext.getUsername())
                    // 设置竞赛预约通知信息
                    .information(ContestRemindUtils.calculateBitMap(requestParam.getRemindTime(), requestParam.getType()))
                    .build();
            contestRemindMapper.insert(contestRemindDO);
        } else {
            // 此时代表要修改或者再次添加已创建过的竞赛预约不同类型、时间提醒
            Long information = contestRemindDO.getInformation();
            Long bitMap = ContestRemindUtils.calculateBitMap(requestParam.getRemindTime(), requestParam.getType());
            /**
             * 这里举个例子：000100（代表创建类型为0，预约时间为 30分钟提醒），但是此时数据已经有这个类型以及时间提醒
             * 000100 & 000100-> != 0L（同时为 1才为 1）
             */
            if ((information & bitMap) != 0L) {
                throw new ClientException("已经创建过该竞赛预约相同类型时间通知!");
            }
            /**
             * 这里举个例子：原本数据库有 000100（代表创建类型为0，预约时间为 30分钟提醒）
             * 新加入一个提醒时间/类型：000010（代表创建类型为0，预约时间为 20分钟的提醒）
             * 000100 ^ 000010 -> （相同为 0，相异为 1）
             * 000110 （代表即有创建类型为0，预约时间为 30分钟提醒，又有创建类型为0，预约时间为 20分钟的提醒）
             */
            contestRemindDO.setInformation(information ^ bitMap);
            contestRemindMapper.update(contestRemindDO, contestRemindQueryWrapper);
        }
        // 向MQ中发送竞赛预约通知延迟消息
        ContestRemindDelayEvent contestRemindDelayEvent = ContestRemindDelayEvent.builder()
                .contestId(requestParam.getContestId())
                .remindTime(requestParam.getRemindTime())
                .type(requestParam.getType())
                .phoneNumber(UserContext.getUsername())
                .startTime(contestDO.getBeginTime())
                // 消息的延迟时间  竞赛正式开始时间 - 设置提醒的时间
                .delayTime(DateUtil.offsetMinute(contestDO.getBeginTime(), -requestParam.getRemindTime()).getTime())
                .build();
        contestRemindDelayProducer.sendMessage(contestRemindDelayEvent);

        // 这里更新了用户预约提醒信息，通过删除缓存来保证数据库与缓存双写一致问题（缓存key:contestID:phoneNumber）
        stringRedisTemplate.delete(RedisCacheConstant.USER_CONTEST_REMIND_INFORMATION +
                requestParam.getContestId() + ":" + UserContext.getUsername());
    }

    @Override
    public void cancelRemindContest(ContestRemindCancelReqDTO requestParam) {
        // 判断该竞赛是否存在或者是已发布
        LambdaQueryWrapper<ContestDO> ContestQueryWrapper = Wrappers.lambdaQuery(ContestDO.class)
                .eq(ContestDO::getId, requestParam.getContestId())
                .eq(ContestDO::getStatus, 1)
                .eq(ContestDO::getDelFlag, 0);
        ContestDO contestDO = contestMapper.selectOne(ContestQueryWrapper);
        if (contestDO != null && contestDO.getBeginTime().before(new Date())) {
            throw new ClientException("无法取消竞赛已经开始的预约通知！");
        }
        // 判断该竞赛用户预约信息是否存在
        LambdaQueryWrapper<ContestRemindDO> contestRemindQueryWrapper = Wrappers.lambdaQuery(ContestRemindDO.class)
                .eq(ContestRemindDO::getPhoneNumber, UserContext.getUsername())
                .eq(ContestRemindDO::getContestId, requestParam.getContestId());
        ContestRemindDO contestRemindDO = contestRemindMapper.selectOne(contestRemindQueryWrapper);
        if (contestRemindDO == null) {
            throw new ClientException("该用户竞赛预约提醒通知信息不存在");
        }
        // 计算一下这个预约点的bitMap
        Long bitMap = ContestRemindUtils.calculateBitMap(requestParam.getRemindTime(), requestParam.getType());
        /**
         * 这里举个例子：000100（代表创建类型为0，预约时间为 30分钟提醒），但是此时数据已经有这个类型以及时间提醒
         * 000100 & 000100 -> （同时为 1才为 1）
         * 结果为：000100 ！= 0L 代表是已经创建过该预约时间
         * 反之：000100 & 000000 -> （同时为 1才为 1）
         * 结果为：000000 -》 0L，代表没有创建过该优惠劵预约时间点
         */
        if ((bitMap & contestRemindDO.getInformation()) == 0L) {
            throw new ClientException("抱歉，您没有预约过该竞赛的时间点提醒");
        }
        /**
         * 这里举个例子：原本数据库有 010100（代表创建类型为0，预约时间为 30、50分钟提醒）
         * 此时要取消这个30分钟时间点的提醒；010100 ^= 000100（相同为 0，相异为 1）
         * 结果为：01000
         */
        bitMap ^= contestRemindDO.getInformation();
        contestRemindQueryWrapper.eq(ContestRemindDO::getInformation, contestRemindDO.getInformation());
        if (bitMap.equals(0L)) {
            // 此时说明该竞赛已无预约时间点
            if (contestRemindMapper.delete(contestRemindQueryWrapper) == 0) {
                // MySQL 排他锁进行删除，如果删除失败，说明用户可能同时正在进行删除、新增提醒操作
                throw new ClientException("取消提醒失败，请刷新页面后重试");
            }
        } else {
            // 此时说明该竞赛还有预约时间点，再进行修改预约时间信息
            contestRemindDO.setInformation(bitMap);
            if (contestRemindMapper.update(contestRemindDO, contestRemindQueryWrapper) == 0) {
                // MySQL 排他锁进行删除，如果删除失败，说明用户可能同时正在进行删除、新增提醒操作
                throw new ClientException("取消提醒失败，请刷新页面后重试");
            }
        }

        // 这里更新了用户预约提醒信息，通过删除缓存来保证数据库与缓存双写一致问题（缓存key:contestID:phoneNumber）
        stringRedisTemplate.delete(RedisCacheConstant.USER_CONTEST_REMIND_INFORMATION +
                requestParam.getContestId() + ":" + UserContext.getUsername());
    }

    @Override
    public boolean isCancelRemind(ContestRemindDTO contestRemindDTO) {
        LambdaQueryWrapper<ContestRemindDO> contestRemindQueryWrapper = Wrappers.lambdaQuery(ContestRemindDO.class)
                .eq(ContestRemindDO::getPhoneNumber, contestRemindDTO.getPhoneNumber())
                .eq(ContestRemindDO::getContestId, contestRemindDTO.getContestId());
        ContestRemindDO contestRemindDO = contestRemindMapper.selectOne(contestRemindQueryWrapper);
        if (contestRemindDO == null) {
            // 此时可能是因为只有一个预约时间点，在取消优惠券预约时间点的时候删除了，该条预约记录
            return true;
        }
        // 此时再去判断需要取消的竞赛时间点是否存在
        Long information = contestRemindDO.getInformation();
        Long bitMap = ContestRemindUtils.calculateBitMap(contestRemindDTO.getRemindTime(), contestRemindDTO.getType());
        // 同时为 1才为 1，没有这个预约时间点就返回 0（PS：消息队列接受到的只有一个时间点）
        return (bitMap & information) == 0L;
    }

    @Override
    public ContestRemindQueryRespDTO someRemindContest(SomeRemindContestReqDTO requestParam) {
        // 判断缓存中是否存在了该用户的预约竞赛信息
        String cacheValue = stringRedisTemplate.opsForValue().get(RedisCacheConstant.USER_CONTEST_REMIND_INFORMATION +
                requestParam.getContestId() + ":" + UserContext.getUsername());
        if (StrUtil.isNotEmpty(cacheValue)) {
            return JSON.parseObject(cacheValue, ContestRemindQueryRespDTO.class);
        }
        // 获取该用户这个竞赛预约通知信息
        LambdaQueryWrapper<ContestRemindDO> contestRemindDOLambdaQueryWrapper = Wrappers.lambdaQuery(ContestRemindDO.class)
                .eq(ContestRemindDO::getPhoneNumber, UserContext.getUsername())
                .eq(ContestRemindDO::getContestId, requestParam.getContestId());
        ContestRemindDO contestRemindDO = contestRemindMapper.selectOne(contestRemindDOLambdaQueryWrapper);
        ContestRemindQueryRespDTO contestRemindQueryRespDTO = new ContestRemindQueryRespDTO();
        if (contestRemindDO != null) {
            // 获取出该竞赛的详情
            LambdaQueryWrapper<ContestDO> queryWrapper = Wrappers.lambdaQuery(ContestDO.class)
                    .eq(ContestDO::getId, contestRemindDO.getContestId())
                    .eq(ContestDO::getDelFlag, 0);
            ContestDO contestDO = contestMapper.selectOne(queryWrapper);
            // 将竞赛详情转化为用户预约竞赛详情返回实体
            contestRemindQueryRespDTO = BeanUtil.toBean(contestDO, ContestRemindQueryRespDTO.class);

            // 开始解析位图结构，进行预约类型预约时间列表填充
            ContestRemindUtils.fillRemindInformationBitMap(contestRemindQueryRespDTO, contestRemindDO.getInformation());

            // 将这个获取到的用户预约竞赛进行缓存存储，将这个设置为竞赛预约信息设置为竞赛结束时间，如果改变了，有双写一致进行解决
            long ttl = (contestDO.getEndTime().getTime() - new Date().getTime()) / 1000;
            stringRedisTemplate.opsForValue().set(RedisCacheConstant.USER_CONTEST_REMIND_INFORMATION + requestParam.getContestId() +
                    ":" + UserContext.getUsername(), JSON.toJSONString(contestRemindQueryRespDTO), ttl, TimeUnit.SECONDS);
        }
        return contestRemindQueryRespDTO;
    }

    @Override
    public ContestFirstQuestionRespDTO firstQuestionContest(Long id) {
        // Redis中存储的题目列表的Key
        String contestCacheKey = String.format(RedisCacheConstant.USER_CONTEST_QUESTION_LIST_KEY, id);
        // 检查缓存中是否存在第一道题目
        String firstQuestionId = stringRedisTemplate.opsForList().index(contestCacheKey, 0);
        if (firstQuestionId != null) {
            // 缓存中存在，直接返回第一题
            return ContestFirstQuestionRespDTO.builder()
                    .id(Long.valueOf(firstQuestionId))
                    .build();
        }

        // 走数据库查询逻辑
        LambdaQueryWrapper<ContestDO> contestQueryWrapper = Wrappers.lambdaQuery(ContestDO.class)
                .eq(ContestDO::getId, id)
                .eq(ContestDO::getStatus, 1)
                .eq(ContestDO::getDelFlag, 0);
        ContestDO contestDO = contestMapper.selectOne(contestQueryWrapper);
        if (contestDO == null) {
            throw new ClientException("未查询到该竞赛详情信息");
        }
        // 查询用户是否报名该竞赛
        LambdaQueryWrapper<UserContestDO> userContestQueryWrapper = Wrappers.lambdaQuery(UserContestDO.class)
                .eq(UserContestDO::getContestId, id)
                .eq(UserContestDO::getPhoneNumber, UserContext.getUsername())
                .eq(UserContestDO::getDelFlag, 0);
        UserContestDO userContestDO = userContestMapper.selectOne(userContestQueryWrapper);
        if (userContestDO == null) {
            throw new ClientException("用户未报名该竞赛，禁止答题！");
        }
        Date now = new Date();
        if (contestDO.getBeginTime().after(now)) {
            throw new ClientException("当前竞赛未开始，禁止答题！");
        }
        if (contestDO.getEndTime().before(now)) {
            throw new ClientException("当前竞赛已结束，禁止答题！");
        }
        // 获取这个竞赛中所有OJ题目列表
        LambdaQueryWrapper<ContestQuestionDO> contestQuestionQueryWrapper = Wrappers.lambdaQuery(ContestQuestionDO.class)
                .eq(ContestQuestionDO::getContestId, id)
                .orderByAsc(ContestQuestionDO::getSortDesc)
                .eq(ContestQuestionDO::getDelFlag, 0);
        List<ContestQuestionDO> contestQuestionList = contestQuestionMapper.selectList(contestQuestionQueryWrapper);
        // 提取 questionId 列表
        List<Long> questionIdList = contestQuestionList.stream()
                .map(ContestQuestionDO::getQuestionId)
                .collect(Collectors.toList());
        stringRedisTemplate.opsForList().rightPushAll(contestCacheKey, questionIdList.stream().map(String::valueOf).toArray(String[]::new));
        stringRedisTemplate.expire(contestCacheKey, (contestDO.getEndTime().getTime() - now.getTime()) / 1000, TimeUnit.SECONDS);
        return ContestFirstQuestionRespDTO.builder()
                .id(questionIdList.get(0))
                .build();
    }

    @Override
    public ContestPrevQuestionRespDTO prevQuestionContest(ContestPrevQuestionReqDTO requestParam) {
        Long prevQuestionId = getAdjacentQuestionId(requestParam.getContestId(), requestParam.getQuestionId(), -1);
        return ContestPrevQuestionRespDTO.builder()
                .id(prevQuestionId)
                .build();
    }

    @Override
    public ContestNextQuestionRespDTO nextQuestionContest(ContestNextQuestionReqDTO requestParam) {
        Long nextQuestionId = getAdjacentQuestionId(requestParam.getContestId(), requestParam.getQuestionId(), 1);
        return ContestNextQuestionRespDTO.builder()
                .id(nextQuestionId)
                .build();
    }

    /**
     * 获取竞赛题目列表中指定题目的前一题或后一题
     *
     * @param contestId         竞赛ID
     * @param currentQuestionId 当前题目ID
     * @param offset            偏移量（-1表示上一题，1表示下一题）
     * @return 偏移后的题目ID
     */
    private Long getAdjacentQuestionId(Long contestId, Long currentQuestionId, int offset) {
        // Redis中存储的题目列表的Key
        String contestCacheKey = String.format(RedisCacheConstant.USER_CONTEST_QUESTION_LIST_KEY, contestId);
        // 获取题目列表大小
        long size = stringRedisTemplate.opsForList().size(contestCacheKey);
        // 遍历找到当前题目ID的位置（索引）
        Long currentIndex = null;
        for (long i = 0; i < size; i++) {
            String questionId = stringRedisTemplate.opsForList().index(contestCacheKey, i);
            if (questionId != null && questionId.equals(String.valueOf(currentQuestionId))) {
                currentIndex = i;
                break;
            }
        }
        // 如果当前题目ID不存在，抛出异常
        if (currentIndex == null) {
            throw new ClientException("当前题目不在竞赛题目列表中！");
        }
        // 计算目标索引，支持循环
        long targetIndex = (currentIndex + offset + size) % size;
        // 返回目标题目的ID
        return Long.valueOf(stringRedisTemplate.opsForList().index(contestCacheKey, targetIndex));
    }
}